Skip to content

Substrate v2: Generic parent-child CRUD app (Phases 2A-1, 2A-2, 2A-3)#33

Open
dadachi wants to merge 9 commits intomainfrom
substrate-v2
Open

Substrate v2: Generic parent-child CRUD app (Phases 2A-1, 2A-2, 2A-3)#33
dadachi wants to merge 9 commits intomainfrom
substrate-v2

Conversation

@dadachi
Copy link
Copy Markdown
Contributor

@dadachi dadachi commented Apr 26, 2026

Summary

Lands the substrate-v2 branch into main. Three sub-phases that together turn the app from the Number-Tag SaaS template into a generic parent-child CRUD shell.

Phase 2A-1 — ItemTag schema to Rails API v2 (#29)

Aligns the Android ItemTag model and serialization with the Rails v2 API contract, ahead of the UI/feature work in 2A-2 and 2A-3.

Phase 2A-2 — Remove NFC / QR / Scan (#31)

Mirrors iOS PR #50. Removes the entire NFC tag-reader, QR-code generator, and Scan tab. After this commit the app is a 2-tab shell (Shops / Settings); ItemTagDetailView becomes a placeholder until 2A-3. Bundled in the same PR: restored swipe complete/idle actions on ShopDetailView (rename resetidle end-to-end, PATCH /shopkeeper/item_tags/:id/idle).

Phase 2A-3 — Generic CRUD UI for ItemTag (#32)

Mirrors iOS PR #53. Replaces the placeholder ItemTagDetailView with a full generic CRUD UI: Create/Edit accept any unicode/symbol name (1–100 chars) plus a multi-line description (0–1000 chars), Detail screen shows a state badge + completed timestamp + Mark-as-completed/idled toggle, list cards surface description preview + state badge.

Threaded changes:

  • ItemTag.position: Int?Int = 0 (server auto-assigns).
  • maximumQueueNumberLengthmaximumNameLength end-to-end (Meta JSON, proto field, datastore, repository).
  • New cardDateTimeString() extension (yyyy/MM/dd HH:mm, locale-independent) used for every completedAt surface (Detail, list card, shop-detail card).

Difference from the paid repo: this Free repo has no per-action permission system (canCreateShops / canUpdateShops / canDeleteShops / canManageTags), so all permission-gating in views and ViewModels is omitted — the FAB, swipe-delete, edit toolbar, and state-toggle button are always shown when signed in.

CI

  • Unit Tests step in .github/workflows/run_tests.yml is commented out — long runtime. spotlessCheck and Android Lint remain active.

Test plan

  • ./gradlew assembleDebug passes
  • ./gradlew test passes (when run locally; commented out in CI)
  • ./gradlew spotlessCheck passes
  • ./gradlew lint passes
  • ./gradlew buildHealth passes
  • Manual smoke test on emulator: sign in; create / edit / mark complete-idle / delete an item tag with unicode + multi-line description; confirm the 2-tab shell renders without Scan; confirm completedAt timestamps render as yyyy/MM/dd HH:mm everywhere.

🤖 Generated with Claude Code

dadachi and others added 9 commits April 25, 2026 07:22
Free-Android port of [nativeapptemplate/NativeAppTemplate-Android#38](nativeapptemplate/NativeAppTemplate-Android#38).

Updates the ItemTag schema to match the Rails API v2:
- rename queue_number -> name
- add description, position
- remove scan_state, customer_read_at, already_completed, display_shop_server_path

Data layer only — Scan/NFC code is kept and adapted to compile against
the new schema. Full Scan/NFC deletion is Phase 2A-2, UI rewrite is
2A-3, further test work is 2A-4.

Key changes
- item_tag_data.proto: queue_number -> name; reserve 5/7/11; add
  description and position.
- Model (Attributes/Data/ItemTag/Shop/ItemTagData/ItemTagBodyDetail):
  field renames; added getName/getDescription/getPosition; dropped
  getScanState/getCustomerReadAt/getAlreadyCompleted/getDisplayShopServerPath.
- Meta.maximumQueueNumberLength default 0 -> 256 so the meta field is
  optional (matches iOS).
- ItemTagCreateViewModel / ItemTagEditViewModel: queueNumber -> name;
  hasInvalidDataQueueNumber -> hasInvalidDataName; updateQueueNumber ->
  updateName.
- Removed production call sites of deleted fields in
  ShowTagInfoScanResultView, ScanViewModel, ShopDetailCardView,
  ShopDetailView — each flagged with a // TODO: removed in Phase 2A-2
  marker.
- Deleted NumberTagsWebpageListView(Model) entirely and removed the
  navigation wiring / ShopSettings entry.
- Updated test fixtures (item_tag.json, item_tags.json) and affected
  unit tests. ScanViewModelTest deleted ahead of 2A-2.
  NumberTagsWebpageListViewModelTest removed.

Verified locally: ./gradlew assembleDebug, test, spotlessCheck all
BUILD SUCCESSFUL.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors NativeAppTemplate-Android PR #39 (which itself mirrors iOS PR #50).
After this, the app is a generic CRUD shell with Shops and Settings tabs;
ItemTagDetailView is a placeholder (name, description, state, completedAt).

### Deletions
- NfcError; ItemTagWriteView/VM + test
- ui/scan/* (ScanView, ScanViewModel, DoScanView, DoScanViewModel,
  CompleteScanResultView, ShowTagInfoScanResultView, ScanNavigation)
  + DoScanViewModelTest
- Models: CompleteScanResult, CompleteScanResultType, ShowTagInfoScanResult,
  ShowTagInfoScanResultType, ItemTagInfoFromNdefMessage, ItemTagType,
  ScanState, ItemTagData
- Proto schemas: scan_result.proto, item_tag_data.proto,
  item_tag_info_from_ndef_message.proto
- res/raw/nfc_reader.json

### Strip / rewrite
- MainActivity: dropped NFC imports, onNewIntent, ACTION_NDEF_DISCOVERED
  handling, singleTask launch mode
- MainActivityViewModel: rewritten minimal (logout + permissions refresh +
  didShowTapShopBelowTip)
- NatAppState / NatNavHost / TopLevelDestination: dropped SCAN_TAB,
  shouldNavigateToScanView flow, navigateToScan helper, scan nav graph
- ShopDetailView / VM: dropped queue instructions block, ReadInstructionsTip,
  didShowReadInstructionsTip; renamed resetItemTag -> idleItemTag and swipe
  action "Reset" -> "Idle" (width 64dp -> 96dp)
- ShopSettingsView / VM: removed the "Reset Number Tags" section, resetShop
  flow
- ItemTagDetailView / VM: placeholder (name, description, state, completedAt)
- LoginRepository (+ Impl) / NatPreferencesDataSource: dropped scan-related
  setters/getters, didShowReadInstructionsTip
- ShopRepository / ShopApi / Impl: dropped resetShop
- ItemTagApi / Repository / Impl: renamed resetItemTag (/reset) ->
  idleItemTag (/idle)
- Utility: dropped scanUri(), extractItemTagInfoFrom(), NFC imports
- NatConstants: dropped SCAN_PATH, SCAN_PATH_CUSTOMER
- UserData: dropped didShowReadInstructionsTip,
  scanViewSelectedTabIndex, shouldFetchItemTagForShowTagInfoScan,
  shouldCompleteItemTagForCompleteScan
- shopSettingsNavigation: dropped ItemTagWriteRoute / itemTagWriteView
- Demo / test repos updated to match trimmed interfaces; obsolete tests
  removed (UtilityTest scanUri, CodedErrorTest NfcError, ItemTagDetailVMTest
  isLock, ShopDetailVMTest reset/didShowReadInstructionsTip)

### Config
- AndroidManifest.xml: removed NFC permission, android.hardware.nfc feature,
  NDEF_DISCOVERED intent-filter, singleTask launch mode
- build.gradle.kts / libs.versions.toml: removed compose-qr-code, capturable,
  lottie-compose
- user_preferences.proto: removed scan fields
- strings.xml: removed ~25 scan/NFC/QR strings; onboarding descriptions 1-13
  rewritten to generic "Welcome to the app." placeholder so OnboardingView
  still compiles (worth redesigning or deleting in a follow-up)

### Test plan
- [x] ./gradlew assembleDebug passes
- [x] ./gradlew test passes
- [x] ./gradlew spotlessCheck passes
- [x] ./gradlew lint passes
- [x] ./gradlew buildHealth passes
- [ ] Manual smoke test on device/emulator: 2 tabs visible (Shops /
  Settings), no Scan tab; sign-in works; ItemTag create / view detail
  placeholder / edit / delete flows work

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors NativeAppTemplate-Android PR #40 (which mirrors iOS PR #50).
After this sub-phase, ItemTag is a generic parent-child CRUD UI:
Create/Edit accept any unicode/symbol name with multi-line description,
Detail screen shows a state badge + completed timestamp + Mark-as-completed/idled toggle,
the list card surfaces description preview + state.

Differences from the paid PR: this Free repo has no per-action permission
system (canCreateShops / canUpdateShops / canDeleteShops / canManageTags),
so all permission-gating in views and ViewModels is omitted — the FAB,
swipe-delete, edit toolbar, and state-toggle button are always shown.

### Data layer
- ItemTag.position: Int? -> Int = 0. Rails server's set_position_if_missing
  always populates this; client trusts the contract.
- ItemTagBodyDetail: dropped position (server auto-assigns).
- Data.getPosition() / ItemTag.getPosition(): now non-null Int.

### Rename maximumQueueNumberLength -> maximumNameLength
- Meta serial name maximum_queue_number_length -> maximum_name_length;
  default fallback 256 -> 100.
- Threaded through Permissions, UserData, user_preferences.proto,
  NatPreferencesDataSource, LoginRepository(+Impl), TestLoginRepository,
  DemoLoginRepository.

### ItemTag Create + Edit
- Validation relaxed: drop alphanumeric and count >= 2 checks. Valid name
  is 1 - maximumNameLength chars (any unicode/symbols/spaces).
- New description field: 0 - 1000 chars, optional, multi-line
  OutlinedTextField (minLines = 4).
  NatConstants.MAXIMUM_ITEM_TAG_DESCRIPTION_LENGTH = 1_000.
- Standard keyboard (no .Ascii).
- hasInvalidData checks both name AND description; Edit's also requires
  that name OR description changed.

### ItemTag Detail full rewrite
- HeaderRow: name + IdlingTag() / CompletedTag() badge.
- DescriptionSection: hidden when blank.
- CompletedAtRow: shown only when state == Completed.
- StateToggleButton: flips between Mark as completed and Mark as idled;
  uses MainButtonView; disabled while isToggling.
- ViewModel: new isToggling, completeItemTag(), idleItemTag(). Errors
  set uiState.message; success silently updates itemTag (matches the
  no-success-toast pattern from #31).

### ItemTag list card
- Headline: name + state badge.
- Supporting: description preview (2 lines, ellipsis) + completed
  timestamp when state == Completed.

### Date helpers
- Add ZonedDateTime.cardDateTimeString() and String.cardDateTimeString()
  to DateUtility.
- Card date format MMM dd yyyy -> yyyy/MM/dd.
- ShopDetailCardView and ItemTagListCardView use cardDateTimeString().

### Strings + constants
- New: name_label, description_label, completed_at_label,
  item_tag_name_placeholder, item_tag_name_is_invalid,
  item_tag_description_is_invalid, item_tag_name_help (format),
  item_tag_description_help (format), mark_as_completed, mark_as_idled.
- Removed queue-specific: tag_number, tag_number_is_invalid, zero_padding.

### Tests
- ItemTagCreateViewModelTest: new validation matrix (unicode/symbols,
  100/101 boundary, 1000/1001 description boundary).
- ItemTagEditViewModelTest: rewritten - unchanged-form invalid,
  description-only change valid, blank name invalid, unicode/symbols valid.
- ItemTagDetailViewModelTest: + completeItemTag / idleItemTag success cases.
- DateUtilityTest: + cardDateTimeString tests.

### Test plan
- [x] ./gradlew clean assembleDebug passes
- [x] ./gradlew test passes
- [x] ./gradlew spotlessCheck passes
- [x] ./gradlew lint passes
- [x] ./gradlew buildHealth passes
- [ ] Manual smoke test on emulator: sign in, create tag with
  "Buy milk 🥛" and multi-line description; mark complete; mark idled;
  edit description; delete

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The test takes a long time to run, so I've commented it out.

spotlessCheck and Android Lint remain active.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flip SwipeableItemWithActions so users drag the row right-to-left to
reveal action buttons, with the buttons sitting on the trailing (right)
edge instead of the leading (left) edge. Matches the iOS app's
swipeActions(edge: .trailing) and the platform-conventional
swipe-to-reveal gesture.

- Actions Row aligned to Alignment.CenterEnd of the wrapping Box
- Drag offset coerced to [-contextMenuWidth, 0f]
- Revealed anchor flipped to -contextMenuWidth; half-way fling threshold
  checks <= -contextMenuWidth / 2f

Public API (isRevealed, actions, onExpanded, onCollapsed, content,
modifier) is unchanged, so both call sites pick up the new direction
with no edits:
- ShopDetailView — Complete / Idle actions on item-tag rows
- ItemTagListView — Delete action on item-tag rows

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two unrelated substrate-v2 cleanups bundled here.

## 1. Drop maximum_name_length from permissions; move to NatConstants

The server no longer sends maximum_name_length in /shopkeeper/permissions;
the client already tolerated its absence via a default. Promote the value
to a constant and drop the dead plumbing.

### Stop reading maximum_name_length from /shopkeeper/permissions
- Meta: drop the @SerialName("maximum_name_length") field.
- Permissions: drop getMaximumNameLength() helper.
- UserData: drop maximumNameLength field.
- user_preferences.proto: reserved 23 (was maximum_name_length) so the
  wire number isn't reused.
- NatPreferencesDataSource: drop the read in the userData mapping, the
  write in setPermissions, and the getMaximumNameLength() flow.
- LoginRepository (interface + impl + DemoLoginRepository +
  TestLoginRepository): drop getMaximumNameLength().

### Move maximumNameLength to NatConstants
- NatConstants: add MAXIMUM_ITEM_TAG_NAME_LENGTH = 100.
- ItemTagCreateViewModel + ItemTagEditViewModel:
  - UiState.maximumNameLength default now reads the constant.
  - Drop the loginRepository constructor param — it was only used for
    getMaximumNameLength().
  - ItemTagCreateViewModel.reload() simplifies to a state reset.
  - ItemTagEditViewModel.fetchData() drops the combine() pairing the
    item-tag flow with the maximum-name-length flow.
- Tests rewritten to drop the now-removed loginRepository wiring and
  the dead .copy(maximumNameLength = 100) calls; new
  maximumNameLength_matchesConstant tests assert against the
  NatConstants value (100).

## 2. Remove "How To Use" entry from Settings

Drop the SettingsView list item linking to myturntag.com/how — a
queue-product help page that no longer matches the substrate-v2
generic CRUD app surface.

The string resource (R.string.how_to_use), the
NatConstants.HOW_TO_USE_URL constant, and the OnboardingView call site
are left in place because they are still wired and may be repurposed.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror the ItemTag pattern (PR #35) for Shop. Server has no caps on Shop
name/description; this is a client-only UX guard.

### Changes
- NatConstants: MAXIMUM_SHOP_NAME_LENGTH = 100,
  MAXIMUM_SHOP_DESCRIPTION_LENGTH = 1_000.
- strings.xml: new shop_name_is_invalid, shop_description_is_invalid,
  shop_name_help, shop_description_help (parametric).
- ShopCreateViewModel + ShopBasicSettingsViewModel:
  - UiState gains maximumNameLength / maximumDescriptionLength
    (defaulted to the constants).
  - hasInvalidData() splits into hasInvalidDataName() +
    hasInvalidDataDescription(); the parent now ORs both.
  - updateName() / updateDescription() reject input over the cap
    (mirrors the existing ItemTagCreateViewModel /
    ItemTagEditViewModel Android pattern).
- ShopCreateView + ShopBasicSettingsView: switch supportingText to the
  two-line layout (always-visible help + conditional red "is invalid"
  line), matching ItemTagCreateView / ItemTagEditView. Description
  switches to heightIn(min=120) + minLines=4.
- Tests: maximumNameLength_matchesConstant /
  maximumDescriptionLength_matchesConstant, boundary tests at
  100 / 101 / 1000 / 1001 chars on both ShopCreateViewModelTest and
  ShopBasicSettingsViewModelTest.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ShopDetailCardView: 2-column layout with description

Mirror iOS ItemTagListCardView's 2-column HStack pattern in
ShopDetailCardView so each item-tag row in ShopDetailView surfaces the
description alongside the name.

Layout:
- Left column (weight 1): name (titleLarge), description (bodySmall,
  max 2 lines, ellipsis) hidden when blank.
- Right column (minWidth 82dp, end-aligned): state tag — CompletedTag +
  completedAt for Completed; IdlingTag for Idled.

Matches iOS ItemTagListCardView's HStack { VStack(alignment: .leading)
[name, description] | Spacer | VStack(alignment: .trailing) [tag,
completedAt] } shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Move description from ShopDetailCardView to ItemTagListCardView

Drop description block from ShopDetailCardView and revert to the
flatter row layout. Restructure ItemTagListCardView from ListItem to
a manual Row with a 2-column layout that surfaces the description.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
)

Port iOS NativeAppTemplate-iOS#61 to Android:
- Drop `scannedItemTagsCount` from Attributes/Data and remove the
  "tags scanned by customers" stat from ShopListCardView.
- Add a left-aligned shop_detail_instruction header to ShopDetailView
  ("Swipe an item tag to change its status.").
- Swap OnboardingView's How-To-Use link for the Support Website link
  and trim onboarding from 13 to 8 descriptions; update placeholder
  strings to match the iOS copy.
- Normalize ItemTag labels: label_add_tag/add_tag_description/
  label_edit_item_tag/title_delete_item_tag and message_item_tag_*
  values now read "Item Tag" / "Item tag" consistently.
- Drop unused HOW_TO_USE_URL, R.string.how_to_use, R.string.learn_more.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant